#!/usr/bin/env python

import os, sys, stat, errno, time
import fuse

# Specify what Fuse API use: 0.2
fuse.fuse_python_api = (0, 2)

import optparse, urlparse, simplejson, traceback, time, logging
from lib import NestedRingLocator, DavManager

logging.basicConfig(filename='power.log', level=logging.DEBUG)


class ConfigException(Exception): pass

class PowerStat(fuse.Stat):
    def __init__(self, **kwargs):
        self.st_mode = 0
        self.st_uid = 0
        self.st_gid = 0
        self.st_size = 0
        # not used yet
        self.st_atime = int(time.time())
        self.st_mtime = self.st_atime
        self.st_ctime = self.st_atime
        # unused attributes need to be there for completeness
        self.st_dev = 0
        self.st_nlink = 2
        self.st_ino = 0
        self.from_dict(kwargs)

    def from_dict(self, props):
        for name, value in props.items():
            setattr(self, 'st_' + name, int(value))

    def to_dict(self):
        props = {}
        for prop in dir(self):
            if not prop.startswith('st_'):
                continue
            props[prop[3:]] = getattr(self, prop)
        return props


class PowerFS(fuse.Fuse):
    def __init__(self, *args, **kwargs):
        config = kwargs.pop('config')
        self.locator = None
        if config:
            globals = {}
            try:
                execfile(config, globals)
                locator = NestedRingLocator(globals['nodes'])
                self.dav = DavManager(locator)
            except:
                raise ConfigException('Config file malformed.')
        if not self.dav:
            raise ConfigException('Config file missing or malformed.')
        self.flags = 0
        self.multithreaded = True
        fuse.Fuse.__init__(self, *args, **kwargs)

    def fsinit(self):
        logging.debug('+PowerFS.fsinit()')
        try:
            self.dav.initialize()
        except:
            logging.exception('-PowerFS.fsinit(\'%s\')' % path)
            return -errno.EIO

    # --- Metadata -----------------------------------------------------------
    def getattr(self, path):
        logging.debug('+PowerFS.getattr(\'%s\')' % path)
        try:
            props = self.dav.get_stat(path)
            if props is None:
                return -errno.ENOENT
            st = PowerStat(**props)
            return st
        except Exception, e:
            logging.exception('-PowerFS.getattr(\'%s\')' % path)
            return -errno.EIO

    def chmod(self, path, mode):
        logging.debug('+PowerFS.chmod(\'%s\', %s)' % (path, mode))
        try:
            self.dav.set_stat(path, mode=mode)
            return 0
        except:
            logging.exception('-PowerFS.chmod(\'%s\')' % path)
            return -errno.EIO

    def chown(self, path, uid, gid):
        logging.debug('+PowerFS.chown(\'%s\', %s, %s)' % (path, uid, gid))
        try:
            kwargs = {}
            if uid != -1:
                kwargs['uid'] = uid
            if gid != -1:
                kwargs['gid'] = gid
            if kwargs:
                self.dav.set_stat(path, **kwargs)
            return 0
        except:
            logging.exception('-PowerFS.chown(\'%s\')' % path)
            return -errno.EIO

    def utime(self, path, times):
        logging.debug('+PowerFS.utime(\'%s\', times=%s)' % (path, str(times)))
        try:
            if times is None:
                mtime = atime = time.time()
            else:
                mtime, atime = times
            self.dav.set_stat(path, mtime=mtime, atime=atime)
        except:
            logging.exception('-PowerFS.utime(\'%s\')' % path)
            return -errno.EIO

    def statfs(self):
        st = fuse.StatVfs()
        st.f_bsize = 65535
        return st

    # --- Namespace ----------------------------------------------------------
    def unlink(self, path):
        logging.debug('+PowerFS.unlink(\'%s\')' % path)
        try:
            self.dav.delete(path)
            return 0
        except:
            logging.exception('-PowerFS.unlink(\'%s\')' % path)
            return -errno.EIO

    def rename(self, src, dst):
        logging.debug('+PowerFS.rename(\'%s\', \'%s\')' % (src, dst))
        try:
            self.dav.move(src, dst)
            return 0
        except:
            logging.exception('-PowerFS.rename(\'%s\', \'%s\')' % (src, dst))
            return -errno.EIO

    # --- Links --------------------------------------------------------------
    def symlink(self, targetPath, linkPath):
        logging.debug('+PowerFS.symlink()')
        return -errno.ENOSYS

    def readlink(self, path):
        logging.debug('+PowerFS.readlink()')
        return -errno.ENOSYS

    # --- Extra Attributes ---------------------------------------------------
    def setxattr(self, path, name, value, flags):
        logging.debug('+PowerFS.setxattr()')
        return -errno.ENOSYS

    def getxattr(self, path, name, size):
        logging.debug('+PowerFS.getxattr()')
        return -errno.ENOSYS

    def listxattr(self, path, size):
        logging.debug('+PowerFS.listxattr()')
        return -errno.ENOSYS

    def removexattr(self, path, name):
        logging.debug('+PowerFS.removexattr()')
        return -errno.ENOSYS

    # --- Files --------------------------------------------------------------
    def mknod(self, path, mode, dev):
        logging.debug('+PowerFS.mknod(\'%s\', %s, %s)' % (path, mode, dev))
        try:
            self.dav.create(path, stat.S_IFREG, mode, dev)
            return 0
        except:
            logging.exception('-PowerFS.mknod()')
            return -errno.EIO

    def truncate(self, path, length):
        logging.debug('+PowerFS.truncate(\'%s\', %s)' % (path, length))
        try:
            self.dav.write(path, '', length)
            return 0
        except:
            logging.exception('-PowerFS.truncate()')
            return -errno.EIO

    def read(self, path, size, offset, fh=None):
        logging.debug('+PowerFS.read(\'%s\', %s, %s, %s)' % (path, size, offset, fh))
        try:
            data = self.dav.read(path, size, offset)
            logging.debug('-PowerFS.read() - %s bytes' % (len(data)))
            return str(data)
        except:
            logging.exception('-PowerFS.read()')
            return -errno.EIO

    def write(self, path, buffer, offset, fh=None):
        logging.debug('+PowerFS.write(\'%s\', %s, %s, %s)' % (path, len(buffer), offset, fh))
        try:
            return self.dav.write(path, buffer, offset)
        except:
            logging.exception('-PowerFS.write()')
            return -errno.EIO

    # --- Directories --------------------------------------------------------
    def mkdir(self, path, mode):
        logging.debug('+PowerFS.mkdir(\'%s\')' % path)
        try:
            self.dav.create(path, stat.S_IFDIR, mode)
            return 0
        except:
            logging.exception('-PowerFS.mkdir()')
            return -errno.EIO

    def rmdir(self, path):
        logging.debug('+PowerFS.rmdir(\'%s\')' % path)
        try:
            self.dav.delete(path)
            return 0
        except:
            logging.exception('-PowerFS.rmdir()')
            return -errno.EIO

    def readdir(self, path, offset):
        logging.debug('+PowerFS.readdir(\'%s\')' % path)
        children = [('.', stat.S_IFDIR), ('..', stat.S_IFDIR)]
        try:
            children.extend(self.dav.readdir(path))
        except:
            logging.exception('-PowerFS.readdir()')
            return -errno.EIO
        return map(lambda (n, t): fuse.Direntry(n, type=t), children)

def main():
    parser = optparse.OptionParser(prog="power", description="Python WebDAV Replicated File System.")
    parser.add_option("-d", "--debug", action="store_true", help="Break into python debugger.")
    parser.add_option("-c", "--config", help="The cluster configuration file.")
    parser.add_option("-l", "--log", help="Log file system information to given file.")
    parser.add_option("-o", help="Mount Options.")

    (options, args) = parser.parse_args()
    args.extend(('-o', options.o))
    
    if not options.config:
        parser.error('Config file is required.')
    if not os.path.exists(options.config):
        parser.error('Config file \'%s\' is missing.' % options.config)

    if options.debug:
        args.append('-f')
        import pdb; pdb.set_trace()

    try:
        fs = PowerFS(config=options.config)
    except ConfigException, e:
        parser.error(str(e))
    fs.parse(args)
    fs.main()


if __name__ == '__main__':
    main()
